package org.jpropelleralt.jview.ref.impl;

import java.awt.Color;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.util.Map;

import javax.swing.JComponent;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.text.JTextComponent;

import org.jpropelleralt.box.Box;
import org.jpropelleralt.box.Boxes;
import org.jpropelleralt.change.Change;
import org.jpropelleralt.jview.JView;
import org.jpropelleralt.ref.Ref;
import org.jpropelleralt.view.View;
import org.jpropelleralt.view.Views;
import org.jpropelleralt.view.update.UpdateManager;

/**
 * An editing {@link View} for a {@link Ref} with value of type {@link String}
 */
public class StringView implements JView {

	private final Ref<String> model;
	private final Ref<Boolean> locked;
	
	private JTextComponent text;
	private JComponent component;
	
	private Color defaultBackground;
	
	private String valueAtStartOfEditing = null;
	
	private final boolean multiline;

	private final UpdateManager updateManager;

	private StringView(Ref<String> model, boolean multiline, Ref<Boolean> locked, UpdateManager updateManager) {

		super();
		this.model = model;
		this.multiline = multiline;
		this.locked = locked;
		buildField();
		
		this.updateManager = updateManager;
		updateManager.registerUpdatable(this);

		model.features().addView(this);
		if (locked != null) {
			locked.features().addView(this);
		}
		
		update();
	}

	/**
	 * Get the {@link JComponent} used for display/editing
	 * @return
	 * 		The text field
	 */
	public JComponent getComponent() {
		return component;
	}
	
	@Override
	public void changes(Map<Box, Change> changes) {
		updateManager.updateRequiredBy(this);
	}
	
	private void buildField() {
		if (multiline) {
			text = new JTextArea(10, 20);
			component = new LinkingJScrollPane(this, text);
		} else {
			JTextField field = new LinkingJTextField(this);
			field.addActionListener(new ActionListener() {
				@Override
				public void actionPerformed(ActionEvent e) {
					commit();
				}
			});
			text = field;
			component = text;
		}
		
		defaultBackground = text.getBackground();
		text.addFocusListener(new FocusListener() {
			//On focus lost, try to commit any pending edit.
			public void focusLost(FocusEvent e) {
				commit();
			}
			public void focusGained(FocusEvent e) {
				//Always update when we get focus
				display();
				
				//Note the value we started from
				valueAtStartOfEditing = text.getText();
			}
		});
	}
	
	private void error(boolean errored) {
		if (errored) {
			text.setBackground(Views.getErrorBackgroundColor());
		} else {
			text.setBackground(defaultBackground);
		}
	}
	
	private void commit() {
		//If we are editing, set the new Ref value
		if (isEditing()) {
			
			//If we are locked, just revert
			if (Boxes.isTrue(locked)) {
				update();
				return;
			}
			
			model.set(text.getText());
			error(false);
		}
	}

	private boolean isEditing() {
		String value = model.get();
		
		//Not editing if value is null
		if (value==null) return false;
		
		//We are editing if text field value is not the same as Ref value
		return (!text.getText().equals(value));
	}

	@Override
	public void update() {
		
		String s = model.get();
		text.setEnabled(s != null && !Boxes.isTrue(locked));
		
		//If the text field is not focused, or value is locked, then always update the display
		//to the new Ref value, if necessary.
		if (!text.isFocusOwner() || Boxes.isTrue(locked)) {
			display();
			
		//If the field is focussed, we only need to update if the new value
		//is different to the valueAtStartOfEditing
		} else {
			if (valueAtStartOfEditing != null && !valueAtStartOfEditing.equals(s)) {
				display();
				//Restart editing
				valueAtStartOfEditing = text.getText();
			}
		}
	}
	
	private void display() {
		//If the text field is not already showing Ref value, 
		//and we are not focussed, update it
		if (isEditing()) {
			String s = model.get();
			
			//Can't display null values
			if (s == null) s = "";
			
			text.setText(s);
			
			//We now know we are not errored, since we are displaying the actual value
			error(false);
		}
	}

	@Override
	public boolean selfNaming() {
		//Just displays value
		return false;
	}
	
	@Override
	public Format format() {
		return Format.SINGLE_LINE;
	}
	
	/**
	 * Create a {@link StringView}, single line
	 * @param model		The value to display
	 * @return			A new {@link StringView}
	 */
	public static StringView create(Ref<String> model) {
		return new StringView(model, false, null, Views.updateManager());
	}

	/**
	 * Create a {@link StringView}
	 * @param model			The value to display
	 * @param multiline		True to use a multiline text component, false for single line 
	 * @return				A new {@link StringView}
	 */
	public static StringView create(Ref<String> model, boolean multiline) {
		return new StringView(model, multiline, null, Views.updateManager());
	}
	
	/**
	 * Create a {@link StringView}
	 * @param model			The value to display
	 * @param multiline		True to use a multiline text component, false for single line 
	 * @param locked		While this is true, view will not permit editing (control disabled) 
	 * @return				A new {@link StringView}
	 */
	public static StringView create(Ref<String> model, boolean multiline, Ref<Boolean> locked) {
		return new StringView(model, multiline, locked, Views.updateManager());
	}

	/**
	 * Create a {@link StringView}
	 * @param model			The value to display
	 * @param multiline		True to use a multiline text component, false for single line 
	 * @param locked		While this is true, view will not permit editing (control disabled) 
	 * @param updateManager	The {@link UpdateManager} to use
	 * @return				A new {@link StringView}
	 */
	public static StringView create(Ref<String> model, boolean multiline, Ref<Boolean> locked, UpdateManager updateManager) {
		return new StringView(model, multiline, locked, updateManager);
	}
	
	/**
	 * This is the same as the parent {@link JComponent}, 
	 * the only difference is that it remembers its {@link JView}, 
	 * so that as long as the {@link JComponent} is in
	 * use, the {@link JView} survives as well. Otherwise, the 
	 * {@link JView} can be GCed while the {@link JComponent} is still 
	 * in use, stopping the {@link JComponent} being updated.
	 * Otherwise, it is very easy for the {@link JView} to be collected,
	 * since a {@link View} is NOT kept alive by the data it is
	 * watching, and is generally not of much interest after its
	 * component has been added to a UI. 
	 */
	public static class LinkingJTextField extends JTextField {
		private final JView owner;
		private LinkingJTextField(JView owner) {
			super();
			this.owner = owner;
		}
		/**
		 * Get the owner {@link JView}
		 * @return	The owner of this component
		 */
		public JView owner() {
			return owner;
		}
	}
	
	/**
	 * This is the same as the parent {@link JComponent}, 
	 * the only difference is that it remembers its {@link JView}, 
	 * so that as long as the {@link JComponent} is in
	 * use, the {@link JView} survives as well. Otherwise, the 
	 * {@link JView} can be GCed while the {@link JComponent} is still 
	 * in use, stopping the {@link JComponent} being updated.
	 * Otherwise, it is very easy for the {@link JView} to be collected,
	 * since a {@link View} is NOT kept alive by the data it is
	 * watching, and is generally not of much interest after its
	 * component has been added to a UI. 
	 */
	public static class LinkingJScrollPane extends JScrollPane {
		private final JView owner;
		private LinkingJScrollPane(JView owner, Component contents) {
			super(contents);
			this.owner = owner;
		}
		/**
		 * Get the owner {@link JView}
		 * @return	The owner of this component
		 */
		public JView owner() {
			return owner;
		}
	}
}
